package edu.gmu.projects.fireresponder.missionplanner;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import edu.gmu.projects.fireresponder.comm.UDPBroadcast;
import edu.gmu.projects.fireresponder.environment.Asset;
import edu.gmu.projects.fireresponder.environment.Building;
import edu.gmu.projects.fireresponder.environment.Location;
import edu.gmu.projects.fireresponder.environment.Path;
import edu.gmu.projects.fireresponder.environment.Route;
import edu.gmu.projects.fireresponder.environment.Safespot;
import edu.gmu.projects.fireresponder.environment.Asset.AssetStatus;
import edu.gmu.projects.fireresponder.fusionengine.IFusionInterface;
import edu.gmu.projects.fireresponder.plan.PlanBuilding;
import edu.gmu.projects.fireresponder.plan.PlanObjective;
import edu.gmu.projects.fireresponder.plan.PlanPath;
import edu.gmu.projects.fireresponder.plan.PlanRoute;
import edu.gmu.projects.fireresponder.plan.PlanSafespot;
import edu.gmu.projects.fireresponder.xml.alerts.AlertGenerator;

public class MissionPlanner {

	public static final double MINIMUM_SAFE_PROBABILITY = .5;

	private Map<String, Asset> mission;
	private SituationEnvironment environment;
	private IFusionInterface fusion;

	public MissionPlanner(final InputStream buildingStream,
			final InputStream pathStream, final InputStream safespotStream) {
		environment = SituationEnvironment.getSituationEnvironment(
				buildingStream, pathStream, safespotStream);
		mission = Collections
				.synchronizedMap(new LinkedHashMap<String, Asset>());
	}

	public void loadPlanFromStream(InputStream planStream) {
		MissionPlan inital = new MissionPlan(planStream);
		mission = Collections
				.synchronizedMap(new LinkedHashMap<String, Asset>());

		// Create mission from keys in initial plan structure
		for (PlanObjective p : inital.getPlanObjectives()) {
			Iterator<String> iter = p.getPlanAssets().iterator();

			while (iter.hasNext()) {
				String key = iter.next();
				Asset asset = null;

				if (p instanceof PlanBuilding) {
					asset = environment.getBuildings().get(key);
				} else if (p instanceof PlanPath) {
					asset = environment.getPaths().get(key);
				} else if (p instanceof PlanRoute) {
					asset = environment.getPaths().get(key);
				} else if (p instanceof PlanSafespot) {
					asset = environment.getSafespots().get(key);
				}

				if (asset != null) {
					mission.put(key, asset);
				}
			}
		}
	}

	public void clearMissionPlan() {
		mission = Collections
				.synchronizedMap(new LinkedHashMap<String, Asset>());
	}

	public void clearAll() {
		clearMissionPlan();
		clearEnvironmentInfo();
	}
	
	public void clearEnvironmentInfo() {
		Iterator<Asset> assets = environment.getAssetSet().iterator();
		
		while (assets.hasNext()) {
			Asset asset = assets.next();
			
			asset.setProbabilityOfFire(0);
			asset.setProbabilityOfPresence(0);
			asset.setRelevanceValue(0);
			asset.setStatus(AssetStatus.NOT_IN_PROGRESS);
		}
	}

	// how are we really going to load this... get it from the FusionService
	public void setFusionEngine(IFusionInterface fusion) {
		this.fusion = fusion;
	}

	public Map<String, Asset> getMission() {
		return mission;
	}

	public Map<String, Asset> getEnvironmentAssetMap() {
		return environment.getAssetMap();
	}

	public Set<Asset> getEnvironmentAssetSet() {
		return environment.getAssetSet();
	}

	public Asset getClosestAsset(Location locationIn) {
		return environment.getClosestAsset(locationIn);
	}

	public Location getCurrentLocationCoordinates() {
		return environment.getSelf().getLocation();
	}

	public Asset getCurrentLocation() {
		return environment.getClosestAsset(getCurrentLocationCoordinates());
	}

	public Map<String, Asset> updateMission() {
		Map<String, Asset> newMission = Collections
				.synchronizedMap(new LinkedHashMap<String, Asset>());
		List<Building> buildingsLeft = getRemainingBuildings();

		SortedSet<PriorityBuilding> buildingPriorities = prioritizeBuildings(buildingsLeft);

		Asset lastLoc = getCurrentLocation();
		for (PriorityBuilding pb : buildingPriorities) {
			Route r = findRouteToBuilding(lastLoc, pb.getBuilding());
			if (r != null) {
				newMission.put(r.getId(), r);
				newMission.put(pb.getBuilding().getId(), pb.getBuilding());
				lastLoc = pb.getBuilding();
			} else {
				String name = environment.getSelf().getId();
				String alert = new AlertGenerator()
						.generateResponderBuildingDroppedAlert(
								name,
								pb.getBuilding().getId(),
								Calendar.getInstance(),
								pb.getBuilding().getCenterPoint().getLatitude(),
								pb.getBuilding().getCenterPoint()
										.getLongitude());
				UDPBroadcast.getUDPBroadcast().send(alert);
			}
		}

		Route escape = findRouteToSafeSpot(lastLoc);
		newMission.put(escape.getId(), escape);
		if (escape != null && !escape.getPaths().isEmpty()) {
			Safespot sp = escape.getPaths().get(escape.getPaths().size() - 1)
					.getAdjacentSafeSpots().iterator().next();
			newMission.put(sp.getId(), sp);
		}
		mission = newMission;
		return newMission;
	}

	private List<Building> getRemainingBuildings() {
		List<Building> buildingsLeft = new ArrayList<Building>();

		for (Asset asset : mission.values()) {
			if (asset instanceof Building) {
				Building building = (Building) asset;
				if (!building.isCleared()) {
					buildingsLeft.add(building);
				}
			}
		}

		return buildingsLeft;
	}

	private boolean isSafe(Path path, int time) {
		double probability = fusion.getFireProbability(path, time * 1000);
		// anything less than 50% we will consider safe
		return probability < MINIMUM_SAFE_PROBABILITY;
	}

	private Route findRouteToBuilding(Asset loc, Building building) {
		Set<Asset> seen = new HashSet<Asset>();
		SortedSet<TimedAsset> unexplored = new TreeSet<TimedAsset>();

		// initialize with available connections...

		// guess we are half done (clearing, along the path, etc)
		int timeToNext = loc.getEstimatedTimeToComplete() / 2;
		seen.add(loc);
		Route startRoute = new Route();
		if (loc instanceof Path) {
			startRoute.addPaths((Path) loc);
		}
		for (Asset a : loc.getConnectedAssets()) {
			seen.add(a);

			if (building.equals(a)) {
				return startRoute;
			}
			if (a instanceof Path) { // currently a route does not allow
				// cutting through buildings to travel
				unexplored.add(new TimedAsset(a, timeToNext, startRoute));
			}
		}

		while (unexplored.size() > 0) {
			TimedAsset ta = unexplored.first();
			unexplored.remove(ta);

			for (Asset a : ta.getAsset().getConnectedAssets()) {
				if (building.equals(a)) {
					return ta.getRouteToHere();
				}
				if (a instanceof Path && !seen.contains(a)) {
					// if path is still safe at this point, consider it
					if (isSafe((Path) a, ta.getTimeToComplete())) {
						unexplored.add(new TimedAsset(a,
								ta.getTimeToComplete(), ta.getRouteToHere()));
					}
				}
				seen.add(a);
			}
		}

		return null;
	}

	private Route findRouteToSafeSpot(Asset loc) {
		Set<Asset> seen = new HashSet<Asset>();
		SortedSet<TimedAsset> unexplored = new TreeSet<TimedAsset>();

		// initialize with available connections...

		// guess we are half done (clearing, along the path, etc)
		int timeToNext = loc.getEstimatedTimeToComplete() / 2;
		seen.add(loc);
		Route startRoute = new Route();

		if (loc instanceof Path) {
			startRoute.addPaths((Path) loc);
		}

		for (Asset a : loc.getConnectedAssets()) {
			seen.add(a);
			if (a instanceof Path) { // currently a route does not allow
				// cutting through buildings to travel
				unexplored.add(new TimedAsset(a, timeToNext, startRoute));
			}
		}

		while (unexplored.size() > 0) {
			TimedAsset ta = unexplored.first();
			unexplored.remove(ta);

			if (ta.getAsset() instanceof Path) {
				if (((Path) ta.getAsset()).isConnectedToSafeSpot()) {
					return ta.getRouteToHere();
				}
			}

			for (Asset a : ta.getAsset().getConnectedAssets()) {
				if (a instanceof Path && !seen.contains(a)) {
					unexplored.add(new TimedAsset(a, ta.getTimeToComplete(), ta
							.getRouteToHere()));
				}
				seen.add(a);
			}
		}

		return null;
	}

	private class TimedAsset implements Comparable<TimedAsset> {

		private int timeToComplete;
		private Asset asset;
		private Route routeToHere;

		public TimedAsset(Asset asset, int timeToStart, Route route) {
			setAsset(asset);
			setTimeToComplete(timeToStart + asset.getEstimatedTimeToComplete());
			Route myRoute = new Route();
			myRoute.addPaths(route.getPaths());
			if (asset instanceof Path) {
				myRoute.addPaths((Path) asset);
			}
			setRouteToHere(myRoute);
		}

		public int getTimeToComplete() {
			return timeToComplete;
		}

		public void setTimeToComplete(int timeToComplete) {
			this.timeToComplete = timeToComplete;
		}

		public Asset getAsset() {
			return asset;
		}

		public void setAsset(Asset asset) {
			this.asset = asset;
		}

		public Route getRouteToHere() {
			return routeToHere;
		}

		public void setRouteToHere(Route routeToHere) {
			this.routeToHere = routeToHere;
		}

		public int compareTo(TimedAsset o) {
			int val = timeToComplete - o.timeToComplete;
			if (val == 0) {
				val = asset.getId().compareTo(o.asset.getId());
			}
			return val;
		}

		public String toString() {
			return timeToComplete + ": " + asset.toString();
		}
	}

	private SortedSet<PriorityBuilding> prioritizeBuildings(
			List<Building> buildings) {
		Map<Building, PriorityBuilding> priorities = new HashMap<Building, PriorityBuilding>();

		int maxTime = 1800000; // 30 minutes

		double minAcceptedFire = .4; // 40% chance of being on fire

		// firePriority = 1-weightForFireNearness + weightFireFireNearness *
		// "nearness"
		double weightForFireNearness = .7;

		// priority = firePriority * fireOnlyWeight + (1 - firePriority) *
		// firePriority * presenseProb
		double fireOnlyWeight = .3;

		for (int time = 0; time <= maxTime && buildings.size() > 0; time += 60000) {
			for (Building building : buildings) {
				if (minAcceptedFire < fusion.getFireProbability(building, time)) {
					// building must be addressed... currently not considering %
					// of building on fire
					double firePriority = (1 - weightForFireNearness)
							+ (weightForFireNearness * (((double) (maxTime - time)) / (double) maxTime));
					double priority = (fireOnlyWeight * firePriority)
							+ ((1 - fireOnlyWeight) * firePriority * fusion
									.getPresenceProbability(building, time));

					priorities.put(building, new PriorityBuilding(building,
							priority, time));
				}
			}
			// do not try to continue prioritize buildings that already have a
			// priority
			buildings.removeAll(priorities.keySet());
		}

		// buildings that won't be on fire in the next 30 minutes
		for (Building building : buildings) {
			double firePriority = 1 - weightForFireNearness;
			double priority = (fireOnlyWeight * firePriority)
					+ ((1 - fireOnlyWeight) * firePriority * fusion
							.getPresenceProbability(building));
			priorities.put(building, new PriorityBuilding(building, priority,
					maxTime));
		}

		SortedSet<PriorityBuilding> sortedSet = new TreeSet<PriorityBuilding>();
		sortedSet.addAll(priorities.values());

		return sortedSet;
	}

	private class PriorityBuilding implements Comparable<PriorityBuilding> {
		private Building building;
		private double priority;
		private long latestTimeToClear;

		public PriorityBuilding(Building building, double priority, long lttc) {
			setBuilding(building);
			setPriority(priority);
			setLatestTimeToClear(lttc);
		}

		public Building getBuilding() {
			return building;
		}

		public void setBuilding(Building building) {
			this.building = building;
		}

		public double getPriority() {
			return priority;
		}

		public void setPriority(double priority) {
			this.priority = priority;
		}

		public long getLatestTimeToClear() {
			return latestTimeToClear;
		}

		public void setLatestTimeToClear(long latestTimeToClear) {
			this.latestTimeToClear = latestTimeToClear;
		}

		public int compareTo(PriorityBuilding o) {
			// higher priority
			int val = (int) (1000 * o.getPriority())
					- (int) (1000 * getPriority());

			// how long I can wait...
			if (val == 0) {
				val = (int) (latestTimeToClear - o.getLatestTimeToClear());
			}

			// should consider how far it is from me?!

			// whatever, break ties
			if (val == 0) {
				val = getBuilding().compareTo(o.getBuilding());
			}
			return val;
		}
	}

	public Set<Asset> getEnvironmentAssets() {
		Set<Asset> assets = new HashSet<Asset>();

		assets.addAll(environment.getBuildings().values());
		assets.addAll(environment.getPaths().values());
		assets.addAll(environment.getSafespots().values());

		return assets;
	}

	public Set<Asset> getMissionAssets() {
		Set<Asset> assets = new HashSet<Asset>();
		assets.addAll(mission.values());
		return assets;
	}
}